from contextlib import contextmanager

from khronos.utils.misc import callable_name


class Channel(object):
    """This class implements a closed channel for asynchronous communication using Signal and 
    Listener primitives. Users are not required to use the primitive classes directly. Instead,
    the listen() and emit() methods should be used.
    
    component_A.py
    
        def bar(signal):
            print signal
            
        channel = Channel()
        channel.listen("foo", callback=bar)  # wait until a matching signal is heard
        
    ... and later, possibly by another component ignorant of the listener
    component_B.py
        
        channel.emit("foo")  # A's listener will be activated now and print the signal
    """
    indent = " " * 2
    
    def __init__(self, log_fnc=None, validation=True):
        self.listeners  = dict()
        self.registered = set()
        self.validation = bool(validation)
        self.stack      = []
        self.log_fnc    = log_fnc
        
    def register(self, type):
        """Register a new signal type in the channel. Channels will not accept signals or 
        listeners of a given type before it is registered."""
        self.registered.add(type)
        
    def unregister(self, type):
        """Remove (if present) a given signal type from the channel."""
        self.registered.discard(type)
        
    def listen(self, type, callback, condition=None, priority=0.0, sticky=True, owner=None):
        """Creates a Listener object and attaches it to the channel. Returns the new listener."""
        listener = Listener(self, type, callback, condition, priority, sticky, owner)
        listener.__deployed = False
        listener.start()
        return listener
        
    def emit(self, type, **payload):
        """Creates a Signal object and broadcasts it on the channel. Returns the new signal."""
        signal = Signal(self, type, **payload)
        signal.emit()
        return signal
        
    @contextmanager
    def emit_block(self, start_type, end_type, **payload):
        """This context manager automatically broadcasts signals at the start and end of a block 
        of code. Signal payload is shared between both signals (which are actually the same 
        object).
        Additionally, the signal is placed in the stack for the duration of the block, causing 
        any other signals or listeners that activate within the block to become indented one 
        level further, helping readability."""
        signal = self.emit(start_type, **payload)
        self.stack.append(signal)
        yield signal
        signal.type = end_type
        signal.emit()
        assert self.stack.pop() is signal
        
    def _insert(self, listener):
        if listener.__deployed:
            raise ValueError("duplicate attempt to deploy listener")
        if self.validation and listener.type is not None and listener.type not in self.registered:
            raise ValueError("unable to listen signal type %r (not registered)" % listener.type)
        try:
            self.listeners[listener.type].add(listener)
        except KeyError:
            self.listeners[listener.type] = set([listener])
        listener.__type = listener.type
        listener.__deployed = True
        
    def _remove(self, listener):
        listener_set = self.listeners[listener.__type]
        listener_set.remove(listener)
        if len(listener_set) == 0:
            del self.listeners[listener.__type]
        listener.__deployed = False
        
    def _broadcast(self, signal):
        """Find and activate all listeners attached to this channel which match 'signal'."""
        if self.validation and signal.type not in self.registered:
            raise ValueError("unable to emit signal type %r (not registered)" % signal.type)
        stack = self.stack
        stack.append(signal)
        log_fnc = self.log_fnc
        indentation = self.indent * (len(stack) - 1)
        if log_fnc is not None:
            log_fnc("%d) %s%s" % (len(stack), indentation, signal))
        indentation += self.indent
        for type in (signal.type, None):
            try:
                listeners = self.listeners[type]
            except KeyError:
                continue
            for listener in sorted(listeners, key=Listener._get_priority, reverse=True):
                if listener.__deployed:
                    stack.append(listener)
                    if log_fnc is not None:
                        log_fnc("%d) %s%s" % (len(stack), indentation, listener))
                    listener._activate(signal)
                    assert stack.pop() is listener
        assert stack.pop() is signal
        
        
class Listener(object):
    """Implements a wait for a signal. Whenever some process broadcasts a signal with the same 
    type on the same channel, the listener will succeed. If the type is None, the listener will 
    succeed at the first signal on the channel, independently of the signal's type. In addition 
    to filtering signals by type, listeners can also have a 'condition' boolean predicate that is 
    able to inspect the contents of matching signals to further narrow down the scope of the 
    listener and only activate when the desired signals are emitted. 
    Listeners also have a 'priority' attribute that defines the order by which listeners are 
    activated when a signal triggers multiple listeners simultaneously (high priority first). 
    Note that activation order between listeners with the same priority is undefined!"""
    def __init__(self, channel, type, callback, condition=None, 
                 priority=0.0, sticky=True, owner=None):
        self.channel = channel      # Channel - the channel where the listener is deployed
        self.type = type            # string (or None) - type of signal that triggers the listener
        self.callback = callback    # callable - function called when the listener is triggered
        self.condition = condition  # callable - predicate used to filter matching signals
        self.priority = priority    # float - defines order of execution of simultaneous triggers
        self.sticky = sticky        # bool - if false, removes itself after activation
        self.owner = owner          # object - something to identify who owns this trigger
        self.match = None           # Signal - signal object that triggered the listener
        
    def __repr__(self):
        filter = "*" if self.type is None else self.type
        condition = "" if self.condition is None else (" if " + callable_name(self.condition))
        owner = ("%s:" % self.owner) if self.owner is not None else ""
        callback = "<NO_CALLBACK>" if self.callback is None else callable_name(self.callback) 
        return ("<%s -%s-%s >> %s%s() [P=%.02f, S=%s] (%08x)>" % 
                (type(self).__name__, filter, condition, owner, callback, 
                 self.priority, self.sticky, id(self)))
        
    def start(self):
        self.channel._insert(self)
        
    def stop(self):
        self.channel._remove(self)
        
    def _activate(self, signal):
        if self.condition is None or self.condition(signal):
            self.match = signal
            self.callback(self)
            if not self.sticky:
                self.stop()
                
    def _get_priority(self):
        """This method is useful only as the 'key' argument to sorted(). For accessing a 
        listener's priority, simple use the 'priority' attribute."""
        return self.priority
        
        
class Signal(object):
    """Signals are an instantaneous primitive that allows synchronizing several components 
    listening on the same channel. Signals activate listeners, which wait until a signal of 
    matching type is emitted on the same channel."""
    def __init__(self, channel, type, **payload):
        if type is None:
            raise ValueError("signal type cannot be None")
        self.channel = channel
        self.type = type
        self.payload = payload
        
    def __repr__(self):
        return "<%s -%s- %r (%08x)>" % (type(self).__name__, self.type, self.payload, id(self))
        
    def emit(self, **new_payload):
        if len(new_payload) > 0:
            self.payload.update(new_payload)
        self.channel._broadcast(self)
        
        
def test_signalling():
    import random
    
    def print_fnc(x):
        print x
    
    def listener(l):
        print "%s activated by %s" % (l, l.match)
        
    c = Channel(log_fnc=print_fnc, validation=True)
    c.register("foo")
    c.listen("foo", listener, owner="bar")
    c.emit("foo", x=0)
    
    for _ in xrange(10):
        c.listen(None, listener, priority=random.uniform(-1, 1), owner="anything goes")
    try:
        c.emit("bar")
    except ValueError, error:
        print error
        c.validation = False
        c.emit("bar")
        